﻿using System.Text;

namespace DJLang
{
    public class DJParser
    {
        public static readonly string SYMBOL_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._+-*/\\<>!?&=";
        public static readonly string UNESCAPABLE_CHARACTERS = "abfnrtv\\\'\"";
        public static readonly string LIST_BEGIN_CHARACTERS = "[{";
        public static IDJValue ParseCmd(string src)
        {
            var expr = new DJExpr();
            int pos = 0;
            while (pos < src.Length)
            {
                IDJValue val = Read(src, ref pos);
                if (val is DJErr err) return err;

                expr.Push(val);
            }

            return expr;
        }

        public static IDJValue Parse(string src)
        {
            int pos = 0;
            return Read(src, ref pos);
        }

        private static IDJValue Read(string src, ref int pos)
        {
            AteWhiteSpace(src, ref pos);

            if (pos >= src.Length)
            {
                return new DJErr("Unexcepted EOF");
            }

            if (src[pos] == ';')
            {
                AteComments(src, ref pos);
                AteWhiteSpace(src, ref pos);
            }

            IDJValue val;
            if (src[pos] == '(')
            {
                pos++;
                val = ReadExpr(src, ref pos);
            }
            else if (src[pos] == '[')
            {
                pos++;
                val = ReadList(src, ref pos, ']');
            }
            else if (src[pos] == '{')
            {
                pos++;
                val = ReadList(src, ref pos, '}');
            }
            else if (src[pos] == '"')
            {
                pos++;
                val = ReadString(src, ref pos);
            }
            else if (src[pos] == '@')
            {
                pos++;
                val = ReadSymbol(src, ref pos, true);
            }
            else if (src[pos] == '#')
            {
                val = ReadBool(src, ref pos);
            }
            else if (SYMBOL_CHARACTERS.Contains(src[pos]))
            {
                val = ReadSymbol(src, ref pos);
            }
            else
            {
                val = new DJErr($"Unexcepted character:{src[pos]}(ASCII:{(int)src[pos]}) at \n{src[(pos-3)..(pos+3)]}\n{new string(' ', 3) + '^'}");
            }

            while (pos < src.Length && char.IsWhiteSpace(src[pos]))
            {
                pos++;
            }

            return val;
        }

        private static IDJValue ReadExpr(string src, ref int pos)
        {
            var expr = new DJExpr();
            while (pos < src.Length && src[pos] != ')')
            {
                IDJValue val = Read(src, ref pos);
                if (val is DJErr err) return err;

                expr.Push(val);

                if (pos >= src.Length)
                {
                    return new DJErr("Syntax error.");
                }
            }
            pos++;
            return expr;
        }

        private static IDJValue ReadList(string src, ref int pos, char end)
        {
            var list = new DJList();
            while (pos < src.Length && src[pos] != end)
            {
                var val = Read(src, ref pos);
                if (val is DJErr err) return err;

                list.Push(val);

                if (pos >= src.Length)
                {
                    return new DJErr("Syntax error.");
                }
            }
            pos++;
            return list;
        }

        private static IDJValue ReadBool(string src, ref int pos)
        {
            IDJValue ans = src[pos] switch
            {
                't' => new DJBasic<bool>(true),
                'f' => new DJBasic<bool>(false),
                _ => new DJErr($"Syntax error, expect t/f got {src[pos]}")
            };
            pos++;
            return ans;
        }

        private static IDJValue ReadSymbol(string src, ref int pos, bool quoted = false)
        {
            var builder = new StringBuilder();

            while (pos < src.Length && SYMBOL_CHARACTERS.Contains(src[pos]))
            {
                builder.Append(src[pos]);
                pos++;
            }

            var symbol = builder.ToString();

            if (int.TryParse(symbol, out int inum))
            {
                return new DJInt(inum);
            }
            else if (float.TryParse(symbol, out float fnum))
            {
                return new DJFloat(fnum);
            }
            else
            {
                return quoted ? new DJQuote(symbol) : new DJSymbol(symbol);
            }
        }

        internal static char GetUnescapeChar(char x) => x switch
        {
            'a' => '\a',
            'b' => '\b',
            'f' => '\f',
            'n' => '\n',
            'r' => '\r',
            't' => '\t',
            'v' => '\v',
            '\\' => '\\',
            '\'' => '\'',
            '\"' => '\"',
            _ => '\0',
        };

        private static IDJValue ReadString(string src, ref int pos)
        {
            var builder = new StringBuilder();

            while (pos < src.Length && src[pos] != '"')
            {
                char c = src[pos];

                if (c == '\\')
                {
                    pos++;
                    if (UNESCAPABLE_CHARACTERS.Contains(src[pos]))
                    {
                        c = GetUnescapeChar(src[pos]);
                    }
                    else
                    {
                        return new DJErr("Invalid escape seq \\" + src[pos]);
                    }
                }

                builder.Append(c);
                pos++;
                if (pos >= src.Length)
                {
                    return new DJErr("Syntax error.");
                }
            }

            // pass final "
            pos++;

            return new DJString(builder.ToString());
        }

        private static void AteComments(string src, ref int pos)
        {
            while (pos < src.Length && src[pos] != '\n')
            {
                pos++;
            }
        }

        private static void AteWhiteSpace(string src, ref int pos)
        {
            while (pos < src.Length && char.IsWhiteSpace(src[pos]))
            {
                pos++;
            }
        }

        public static bool IsBrackets(char ch)
        {
            return ch == '(' || ch == ')' || ch == '[' || ch == ']';
        }

        public static bool IsStringQuote(char ch)
        {
            return ch == '"';
        }
    }
}
